import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.Queue;

import org.yaml.snakeyaml.Yaml;

public class MessagePasser {
	private String local_name;
	private int id;
	private String clockType;
	private ClockService clock;

	private HashMap<String, Node> nodes;
	private HashMap<Rule, Integer> sendRules;
	private HashMap<Rule, Integer> receiveRules;
	private HashMap<String, Sender> senders;
	private Queue<Message> receiveQueue;
	private Queue<Message> delaySendQueue;
	private Queue<Message> delayReceiveQueue;

	public MessagePasser(String configuration_filename, String local_name) {
		this.local_name = local_name;
		id = 0;
		clockType = null;
		nodes = new HashMap<String, Node>();
		sendRules = new HashMap<Rule, Integer>();
		receiveRules = new HashMap<Rule, Integer>();
		senders = new HashMap<String, Sender>();
		receiveQueue = new LinkedList<Message>();
		delaySendQueue = new LinkedList<Message>();
		delayReceiveQueue = new LinkedList<Message>();

		parseConfigurationFile(configuration_filename);
		setUpClock();
		setUpSocket();
	}

	public void send(Message message) {

		if (clockType != null) {
			clock.increase();
			message = new TimeStampedMessage(message);
			clock.assign((TimeStampedMessage) message);
			// Sender loggerSender = senders.get("logger");
			// loggerSender.send(message);
		}
		String dest = message.getDest();
		if (senders.containsKey(dest)) {
			Sender sender = senders.get(dest);
			message.set_id(++id);

			String action = checkRules(message, sendRules);
			System.out.println("send action: " + action);
			sendWithRule(message, sender, action);
		}
	}
	
	public void multicast(Object object) {
		String kind = "multicast";
		for (Entry<String, Node> e : nodes.entrySet()) {
			String dest = e.getKey();

			Message m = new Message(local_name, dest, kind, object);
			send(m);
		}
	}

	public Message receive() {
		Message m = receiveQueue.poll();
		if (m != null && clockType != null) {
			clock.update((TimeStampedMessage) m);
			clock.increase();
		}
		return m;

	}
	
	public void dump() {
		ArrayList<TimeStampedMessage> dumpList = new ArrayList<TimeStampedMessage>();
		for (Message m : receiveQueue) {
			dumpList.add((TimeStampedMessage) m);
		}
		Collections.reverse(dumpList);
		Collections.sort(dumpList, new TimeStampComparator());
		for (Message m : dumpList) {
			System.out.println(m);
		}
	}



	private void setUpClock() {
		clock = ClockService.getClock(clockType, nodes, local_name);
	}

	private void setUpSocket() {
		new Server(receiveQueue, nodes.get(local_name).getPort(), nodes.size(),
				receiveRules, delayReceiveQueue, this).start();
		new Client(nodes, senders).start();
		System.out.println(local_name + " is set up.");
	}

	private void parseConfigurationFile(String configuration_filename) {
		try {
			Yaml yaml = new Yaml();
			@SuppressWarnings("unchecked")
			HashMap<String, Object> map1 = (HashMap<String, Object>) yaml
					.load(new FileInputStream(configuration_filename));
			if (map1.containsKey("Configuration")) {
				@SuppressWarnings("unchecked")
				ArrayList<HashMap<String, Object>> list1 = (ArrayList<HashMap<String, Object>>) map1
						.get("Configuration");
				for (HashMap<String, Object> map2 : list1) {
					String name = (String) map2.get("Name");
					String ip = (String) map2.get("IP");
					int port = (int) map2.get("Port");

					nodes.put(name, new Node(name, ip, port));
				}
			}
			if (map1.containsKey("Clock")) {
				clockType = (String) map1.get("Clock");

			}
			if (map1.containsKey("SendRules")) {
				@SuppressWarnings("unchecked")
				ArrayList<HashMap<String, Object>> list2 = (ArrayList<HashMap<String, Object>>) map1
						.get("SendRules");
				for (HashMap<String, Object> map3 : list2) {
					String action = null;
					String src = null;
					String dest = null;
					String kind = null;
					int id = -1;
					int nth = -1;

					if (map3.containsKey("Action"))
						action = (String) map3.get("Action");
					if (map3.containsKey("Src"))
						src = (String) map3.get("Src");
					if (map3.containsKey("Dest"))
						dest = (String) map3.get("Dest");
					if (map3.containsKey("Kind"))
						kind = (String) map3.get("Kind");
					if (map3.containsKey("ID"))
						id = (int) map3.get("ID");
					if (map3.containsKey("Nth"))
						nth = (int) map3.get("Nth");

					sendRules
							.put(new Rule(action, src, dest, kind, id, nth), 0);
				}
			}
			if (map1.containsKey("ReceiveRules")) {
				@SuppressWarnings("unchecked")
				ArrayList<HashMap<String, Object>> list3 = (ArrayList<HashMap<String, Object>>) map1
						.get("ReceiveRules");
				for (HashMap<String, Object> map4 : list3) {
					String action = null;
					String src = null;
					String dest = null;
					String kind = null;
					int id = -1;
					int nth = -1;

					if (map4.containsKey("Action"))
						action = (String) map4.get("Action");
					if (map4.containsKey("Src"))
						src = (String) map4.get("Src");
					if (map4.containsKey("Dest"))
						dest = (String) map4.get("Dest");
					if (map4.containsKey("Kind"))
						kind = (String) map4.get("Kind");
					if (map4.containsKey("ID"))
						id = (int) map4.get("ID");
					if (map4.containsKey("Nth"))
						nth = (int) map4.get("Nth");

					receiveRules.put(
							new Rule(action, src, dest, kind, id, nth), 0);
				}

			}

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}

	private void sendWithRule(Message message, Sender sender, String action) {

		if (action == null) {
			sender.send(message);
			if (!delaySendQueue.isEmpty()) {
				sender.send(delaySendQueue.poll());
			}
		} else if (action.equals("drop")) {

		} else if (action.equals("duplicate")) {
			sender.send(message);
			sender.send(message);
			if (!delaySendQueue.isEmpty()) {
				sender.send(delaySendQueue.poll());
			}
		} else if (action.equals("delay")) {
			delaySendQueue.offer(message);
		}
	}

	private String checkRules(Message m, HashMap<Rule, Integer> sendRules) {
		for (Entry<Rule, Integer> e : sendRules.entrySet()) {
			Rule r = e.getKey();
			if (obeyRule(m, r)) {
				sendRules.put(r, sendRules.get(r) + 1);
				if (r.getNth() == -1
						|| (r.getNth() != -1 && r.getNth() == e.getValue()))
					return r.getAction();
			}
		}

		return null;
	}

	private boolean obeyRule(Message m, Rule r) {
		// System.out.println(r);
		if (r.getSrc() != null && !m.getSrc().equals(r.getSrc()))
			return false;
		if (r.getDest() != null && !m.getDest().equals(r.getDest()))
			return false;
		if (r.getKind() != null && !m.getKind().equals(r.getKind()))
			return false;
		if (r.getId() != -1 && m.getId() != r.getId())
			return false;

		return true;
	}

	

	public String getLocal_name() {
		return local_name;
	}

	public void setLocal_name(String local_name) {
		this.local_name = local_name;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getClockType() {
		return clockType;
	}

	public void setClockType(String clockType) {
		this.clockType = clockType;
	}

	public ClockService getClock() {
		return clock;
	}

	public void setClock(ClockService clock) {
		this.clock = clock;
	}

}
